package edu.mcw.rgd.clinminer.web.userrole;

import com.vaadin.ui.AbstractSelect;
import com.vaadin.ui.AbstractSelect.NewItemHandler;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.ComboBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Layout;
import com.vaadin.ui.PopupView;
import com.vaadin.ui.Table;
import com.vaadin.ui.Table.CellStyleGenerator;
import com.vaadin.ui.TreeTable;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.Reindeer;

import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.apache.log4j.Logger;

import com.vaadin.addon.jpacontainer.EntityItem;
import com.vaadin.addon.jpacontainer.JPAContainer;
import com.vaadin.addon.tableexport.ExcelExport;
import com.vaadin.data.Container;
import com.vaadin.data.Container.Filter;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.util.HierarchicalContainer;
import com.vaadin.data.util.filter.Or;
import com.vaadin.data.util.filter.SimpleStringFilter;

import edu.mcw.rgd.clinminer.domain.view.Annotation;
import edu.mcw.rgd.clinminer.web.AbstractEntityView;
import edu.mcw.rgd.clinminer.web.EntityEditor;
import edu.mcw.rgd.clinminer.web.EntityProviderUtil;
import edu.mcw.rgd.clinminer.web.utils.NotificationInfoMiddle;

public class SearchView extends
		AbstractEntityView<edu.mcw.rgd.clinminer.domain.view.Annotation> {
	private static final Logger log = Logger.getLogger(SearchView.class);

	private Boolean areResultsGrouped = true;
	private Boolean areResultsCollapsed;

	private JPAContainer<Annotation> jpaContainer;
	private HierarchicalContainer hierarchicalContainer;

	@Override
	protected EntityEditor createForm() {
		return new DummyEntityEditor();
	}

	// make sure both are initialized postconstruct
	// field initializaton not working in time (spring?)
	private Boolean areResultsGrouped() {
		if (areResultsGrouped == null) {
			areResultsGrouped = true;
		}
		return areResultsGrouped;
	}

	private Boolean areResultsCollapsed() {
		if (areResultsCollapsed == null) {
			areResultsCollapsed = false;
		}
		return areResultsCollapsed;
	}

	@Override
	protected void configureTable(Table table) {
		// configure table
		updateTableContainer();
		table.setCaption(null);
		table.setSizeFull();
		table.setSelectable(false);
		setupGeneratedColumns(table);
		table.setColumnHeader("ontologyTermLabel", "Phenotype");
		table.setColumnHeader("LOINCLabel", "Measurement");
	}

	@Override
	public Object[] getTableColumns() {
		String availableProps = "";
		for (Object property : getJPAContainer().getContainerPropertyIds()) {
			availableProps += " " + (String) property;
		}
		// log.debug("Available properties to display: " + availableProps);

		String[] props = new String[] {
				"participantName",
				"ontologyTermLabel",
				"LOINCLabel",
				"value",
				"unit",
				"eventDate",
				"documentSourceDate",
				"notes",
				"annotatedText",
				"testedPhenotypeLabel",
				"documentSource",
				"LOINCID",
				"ontologyTermID" };
		return props;
	}

	private class ValueComparator implements Comparator<String> {

		Map<String, Integer> base;

		public ValueComparator(Map<String, Integer> base) {
			this.base = base;
		}

		@Override
		public int compare(String a, String b) {
			if (base.get(a) >= base.get(b)) {
				return -1;
			} else {
				return 1;
			}
		}
	}

	private ComboBox buildSearchBoxCombo(final Label facetedLabel,
			final PopupView popup) {
		// Alternatively build a UNION view in oracle
		final List<String> captions = new ArrayList<String>();
		final Map<String, Integer> suggestions = new HashMap<String, Integer>();
		final ComboBox searchBox = new ComboBox();

		// buld suggestion list
		for (Annotation a : Annotation.findAllAnnotations()) {
			for (java.lang.reflect.Method m : a.getClass().getMethods()) {
				// call all getters that return a string, integer or date
				if (m.getName().startsWith("get")
						&& (m.getReturnType() == String.class || m
								.getReturnType() == Integer.class)) {
					if (m.getName().equals("getVersion")
							|| m.getName().equals("getId")) {
						continue;
					}
					Object value = null;
					try {
						value = m.invoke(a, (Object[]) null);
					} catch (IllegalAccessException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (IllegalArgumentException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (InvocationTargetException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					if (value != null) {
						String key = value.toString().trim();

						Integer count = suggestions.get(key);
						if (count == null) {
							count = 0;
						}
						suggestions.put(key, ++count);
					}
				}
			}
		}

		ValueComparator bvc = new ValueComparator(suggestions);
		TreeMap<String, Integer> sorted_map = new TreeMap<String, Integer>(bvc);
		sorted_map.putAll(suggestions);

		// add suggestions
		for (String key : sorted_map.keySet()) {
			searchBox.addItem(key);
			String caption = key + " (" + suggestions.get(key) + ")";
			searchBox.setItemCaption(key, caption);
			captions.add(caption);
		}

		searchBox.setNewItemsAllowed(true);
		searchBox.setNewItemHandler(new NewItemHandler() {

			@Override
			public void addNewItem(String newItemCaption) {

				if (!captions.contains(newItemCaption)) {
					searchBox.addItem(newItemCaption);
					captions.add(newItemCaption);
					searchBox.setItemCaption(newItemCaption, newItemCaption);
					searchBox.setValue(newItemCaption);
				}
			}
		});

		searchBox
				.setFilteringMode(AbstractSelect.Filtering.FILTERINGMODE_CONTAINS);

		searchBox.setWidth(350, UNITS_PIXELS);
		searchBox.setImmediate(true);
		searchBox.setInputPrompt("enter your query, e.g., rash...");
		searchBox.addListener(new Property.ValueChangeListener() {

			@Override
			public void valueChange(ValueChangeEvent event) {
				String text = "";
				if (event.getProperty().getValue() != null) {
					text = event.getProperty().getValue().toString();
				}
				final String highlightedText = text;

				// clean all captions
				// update this one so that user doesn't see the item count
				// on blur any more
				for (Entry<String, Integer> e : suggestions.entrySet()) {
					String caption = e.getKey() + " (" + e.getValue() + ")";
					if (e.getKey().equals(highlightedText)) {
						caption = e.getKey();
					}
					searchBox.setItemCaption(e.getKey(), caption);
				}

				// highlight cells with matching text
				getTable().setCellStyleGenerator(new CellStyleGenerator() {
					@Override
					public String getStyle(Object itemId, Object propertyId) {
						if (propertyId == null || highlightedText.equals("")) {
							return null;
						}
						try {
							Object value = getJPAContainer().getItem(itemId)
									.getItemProperty(propertyId).getValue();

							if (value != null && value.toString() != null // unclear
																			// why
																			// this
																			// is
																			// needed
									&& value.toString()
											.toUpperCase()
											.contains(
													highlightedText
															.toUpperCase())) {
								return "c-highlight-text";
							} else {
								return null;
							}
						} catch (java.lang.IllegalArgumentException e) {
							// ignore grouping items
							return null;
						}
					}
				});

				// Create an Oring filter including all props
				List<Filter> list = new ArrayList<Filter>();
				for (Object prop : getTableColumns()) {
					// important as otherwise breaks the query
					if ("participant".equals(prop)) {
						continue;
					}
					// ignore dates as need to do conversion for comparison
					if (prop.toString().toUpperCase().contains("DATE")) {
						continue;
					}
					list.add(new SimpleStringFilter(prop.toString(),
							highlightedText, true, false));
				}
				Filter filterArray = new Or(
						list.toArray(new Filter[list.size()]));
				getJPAContainer().removeAllContainerFilters();
				getJPAContainer().addContainerFilter(filterArray);

				// update table container
				hierarchicalContainer = null;
				updateTableContainer();

				// faceted search
				updateFacetedReport(facetedLabel, popup, highlightedText);
			}
		});

		return searchBox;
	}

	private Layout buildSearchLayout() {
		Label spacer2 = new Label("");
		spacer2.setHeight(3, UNITS_EM);
		final Label facetedLabel = new Label("");
		final PopupView popup = new PopupView("more...", new Label("More info"));

		// facetedSearch.setStyleName(Reindeer.LABEL_SMALL);
		VerticalLayout facetedLayout = new VerticalLayout();
		facetedLabel.setContentMode(Label.CONTENT_PREFORMATTED);
		popup.setHideOnMouseOut(false);
		popup.setVisible(false);
		facetedLayout.addComponent(facetedLabel);
		facetedLayout.addComponent(popup);
		facetedLayout.setComponentAlignment(popup, Alignment.BOTTOM_RIGHT);
		facetedLayout.setSpacing(true);
		updateFacetedReport(facetedLabel, popup, "");

		// searchBox.focus();
		ComboBox searchBox = buildSearchBoxCombo(facetedLabel, popup);

		Button searchButton = new Button("ClinMynEHR Search");
		HorizontalLayout mainLayout = new HorizontalLayout();
		Layout searchLayout = new VerticalLayout();

		// faceted navigation goes to the right
		mainLayout.addComponent(searchLayout);
		mainLayout.addComponent(facetedLayout);
		mainLayout.setSpacing(true);

		searchLayout.addComponent(searchBox);
		searchLayout.addComponent(spacer2);
		searchLayout.addComponent(searchButton);

		((VerticalLayout) searchLayout).setComponentAlignment(searchBox,
				Alignment.MIDDLE_CENTER);
		((VerticalLayout) searchLayout).setComponentAlignment(searchButton,
				Alignment.MIDDLE_CENTER);
		searchLayout.setSizeUndefined();
		return mainLayout;
	}

	private void updateFacetedReport(Label facetedLabel, PopupView popup,
			String highlightedText) {
		Set<String> uniqueParticipants = new HashSet<String>();
		Map<String, Integer> matchedProps = new HashMap<String, Integer>();
		// iterate over all items and their properties
		for (Object itemId : getJPAContainer().getItemIds()) {
			for (Object propertyId : getTableColumns()) {
				Property p = getJPAContainer().getContainerProperty(itemId,
						propertyId);
				if ("participantName".equals(propertyId)) {
					uniqueParticipants.add(p.getValue().toString());
				}
				// ignore dates as need to do conversion for comparison
				if (propertyId.toString().toUpperCase().contains("DATE")) {
					continue;
				}
				if (p.getValue() == null) {
					continue;
				}
				if ("id".equals(propertyId)) {
					continue;
				}
				// count which properties matched
				String value = p.getValue().toString();
				if (value.toUpperCase().contains(highlightedText.toUpperCase())) {
					Integer count = matchedProps.get(propertyId);
					if (count == null) {
						count = 0;
					}
					matchedProps.put(propertyId.toString(), ++count);
				}
			}
		}

		// update view
		// convert props to text
		String summaryText = "(" + uniqueParticipants.size()
				+ ")\tparticipants" + "\n(" + getJPAContainer().size()
				+ ")\tannotations";
		facetedLabel.setValue(summaryText);

		String detailedText = "";
		for (Entry<String, Integer> e : matchedProps.entrySet()) {
			if (e.getValue() > 0) {
				detailedText += "(" + e.getValue() + ")\t"
						+ e.getKey().toUpperCase() + "\n";
			}
		}
		final Label popupLabel = new Label(detailedText,
				Label.CONTENT_PREFORMATTED);
		popupLabel.setWidth(200, UNITS_PIXELS);

		PopupView.Content content = new PopupView.Content() {
			@Override
			public Component getPopupComponent() {
				return popupLabel;
			}

			@Override
			public String getMinimizedValueAsHTML() {
				return "details...";
			}
		};
		popup.setContent(content);
		popup.setVisible(true);

		// show notification with results summary
		if (getApplication() != null) {
			getApplication().getMainWindow().showNotification(
					new NotificationInfoMiddle(uniqueParticipants.size()
							+ " participants", getJPAContainer().size()
							+ " annotations"));
		}
	}

	@Override
	public Layout getViewLayout() {
		VerticalLayout layout = new VerticalLayout();
		Label spacer1 = new Label("");
		spacer1.setHeight(3, UNITS_EM);
		Label spacer2 = new Label("");
		spacer2.setHeight(3, UNITS_EM);

		Layout searchLayout = buildSearchLayout();
		layout.addComponent(spacer1);
		layout.addComponent(searchLayout);
		layout.addComponent(spacer2);

		Layout buttonLayout = buildButtonLayout();
		layout.addComponent(buttonLayout);

		layout.addComponent(getTable());

		layout.setExpandRatio(getTable(), 1.0f);
		layout.setComponentAlignment(searchLayout, Alignment.TOP_CENTER);
		layout.setComponentAlignment(buttonLayout, Alignment.MIDDLE_CENTER);
		layout.setSizeFull();
		return layout;
	}

	private Layout buildButtonLayout() {
		HorizontalLayout layout = new HorizontalLayout();
		final String bGroupCaptionOn = "Ungroup Results";
		final String bGroupCaptionOff = "Group by Participant";
		final String bCollapseCaptionOn = "Collapse All";
		final String bCollapseCaptionOff = "Expand All";

		Button bGroup = new Button(bGroupCaptionOn);
		final Button bCollapse = new Button(bCollapseCaptionOn);

		bGroup.setStyleName(Reindeer.BUTTON_LINK);
		bGroup.addListener(new ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				// toggle
				areResultsGrouped = !areResultsGrouped;

				updateTableContainer();

				if (areResultsGrouped()) {
					event.getButton().setCaption(bGroupCaptionOn);
					bCollapse.setEnabled(true);
				} else {
					event.getButton().setCaption(bGroupCaptionOff);
					bCollapse.setEnabled(false);
				}
			}
		});

		bCollapse.setStyleName(Reindeer.BUTTON_LINK);
		bCollapse.addListener(new ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				areResultsCollapsed = !areResultsCollapsed;

				for (Object itemId : ((TreeTable) getTable()).rootItemIds()) {
					((TreeTable) getTable()).setCollapsed(itemId,
							areResultsCollapsed);
				}

				if (areResultsCollapsed) {
					event.getButton().setCaption(bCollapseCaptionOff);
				} else {
					event.getButton().setCaption(bCollapseCaptionOn);
				}
			}
		});

		Button bExport = new Button("Export to Excel");
		bExport.setStyleName(Reindeer.BUTTON_LINK);
		bExport.addListener(new ClickListener() {
			@Override
			public void buttonClick(ClickEvent event) {
				ExcelExport excelExport = new ExcelExport(getTable());
				excelExport.setExportFileName("ClinMynEHR_report_"
						+ Calendar.getInstance().getTime().toString());
				excelExport.excludeCollapsedColumns();
				excelExport.setReportTitle("ClinMynEHR_report"
						+ Calendar.getInstance().getTime().toString());
				excelExport.export();
			}
		});

		layout.addComponent(bGroup);
		layout.addComponent(new Label(" | "));
		layout.addComponent(bCollapse);
		layout.addComponent(new Label(" | "));
		layout.addComponent(bExport);
		layout.setSpacing(true);
		layout.setMargin(true);
		layout.setComponentAlignment(bGroup, Alignment.MIDDLE_CENTER);
		layout.setComponentAlignment(bCollapse, Alignment.MIDDLE_CENTER);

		return layout;
	}

	@Override
	protected Table createTable() {
		TreeTable table = new TreeTable("Formatted Table") {
			private static final long serialVersionUID = 1L;

			@Override
			protected String formatPropertyValue(Object rowId, Object colId,
					Property property) {
				// Format by property type
				if (property.getType() == Date.class
						&& null != property.getValue()) {
					SimpleDateFormat df = new SimpleDateFormat("MM/dd/yyyy");
					return df.format((Date) property.getValue());
				}
				return super.formatPropertyValue(rowId, colId, property);
			}
		};

		table.setColumnCollapsingAllowed(true);
		table.setColumnReorderingAllowed(true);
		return table;
	}

	public void setupGeneratedColumns(Table table) {
		// table.removeGeneratedColumn("studies");
		// table.addGeneratedColumn("studies", new EntityTableColumnGenerator(
		// "name"));
	}

	@Override
	public String getEntityName() {
		return "Annotation";
	}

	@Override
	public Class<Annotation> getEntityClass() {
		return Annotation.class;
	}

	@Override
	public boolean isCreateAllowed() {
		return false;
	}

	@Override
	public boolean isUpdateAllowed() {
		return false;
	}

	@Override
	public boolean isDeleteAllowed() {
		return false;
	}

	public List<Annotation> listEntities() {
		List<Annotation> list = Annotation.findAllAnnotations();
		return list;
	}

	@Override
	public Annotation saveEntity(Annotation entity) {
		if (entity == null) {
			return null;
		}
		return (Annotation) entity.merge();
	}

	@Override
	public void deleteEntity(Annotation entity) {
		if (entity != null) {
			entity.remove();
		}
	}

	@Override
	public boolean isNewEntity(Annotation entity) {
		return (entity != null && getIdForEntity(entity) == null);
	}

	@Override
	public String getIdProperty() {
		return "id";
	}

	@Override
	public String getVersionProperty() {
		return "version";
	}

	@Override
	public Annotation createEntityInstance() {
		return new Annotation();
	}

	private JPAContainer<Annotation> getJPAContainer() {
		if (jpaContainer == null) {
			JPAContainer<Annotation> container = new JPAContainer<Annotation>(
					Annotation.class);
			container.setEntityProvider(EntityProviderUtil.get()
					.getEntityProvider(Annotation.class));
			jpaContainer = container;
		}
		return jpaContainer;
	}

	private HierarchicalContainer getHierarchicalContainer() {
		if (hierarchicalContainer == null) {
			hierarchicalContainer = new HierarchicalContainer();

			hierarchicalContainer.addContainerProperty("participantName",
					Layout.class, null);
			Map<Object, Object> uniqueParticipants = new HashMap<Object, Object>();
			// set property values
			for (Object propertyId : getTableColumns()) {
				if (propertyId.equals("participant")
						|| propertyId.equals("participantName")) {
					continue; // already added
				}
				hierarchicalContainer.addContainerProperty(propertyId,
						getJPAContainer().getType(propertyId), null);
			}

			for (Object itemId : getJPAContainer().getItemIds()) {
				// copy items into container
				hierarchicalContainer.addItem(itemId);
				hierarchicalContainer.setChildrenAllowed(itemId, false);
				for (Object propertyId : getTableColumns()) {
					Property p = getJPAContainer().getContainerProperty(itemId,
							propertyId);

					// get unique participants
					if ("participantName".equals(propertyId)) {
						Object parentId = uniqueParticipants.get(p.getValue());
						if (parentId == null) {
							parentId = hierarchicalContainer.addItem();
							Layout layout = new HorizontalLayout();
							Label label = new Label(p.getValue().toString());
							CheckBox checkBox = new CheckBox();
							layout.addComponent(checkBox);
							layout.addComponent(label);

							hierarchicalContainer.getContainerProperty(
									parentId, "participantName").setValue(
									layout);
							uniqueParticipants.put(p.getValue(), parentId);
						}
						hierarchicalContainer.setParent(itemId, parentId);
					} else if ("participant".equals(propertyId)) {
						continue;
					} else {
						// copy properties into container
						hierarchicalContainer.getContainerProperty(itemId,
								propertyId).setValue(p.getValue());
					}
				}
			}
		}
		return hierarchicalContainer;
	}

	@Override
	public Container getTableContainer() {
		if (areResultsGrouped()) {
			return getHierarchicalContainer();
		} else {
			return getJPAContainer();
		}
	}

	private void updateTableContainer() {
		Container container = getTableContainer();
		getTable().setContainerDataSource(container);
		getTable().setVisibleColumns(getTableColumns());

		if (areResultsGrouped()) {
			// toggle expansion status
			for (Object itemId : container.getItemIds()) {
				((TreeTable) getTable()).setCollapsed(itemId,
						areResultsCollapsed());
			}
		}
	}

	@Override
	public Item getItemForEntity(Annotation entity) {
		Item item = getTable().getItem(entity.getId());
		if (item == null && getTableContainer() instanceof JPAContainer) {
			item = ((JPAContainer<Annotation>) getTableContainer())
					.createEntityItem(entity);
		}
		return item;
	}

	@Override
	public Annotation getEntityForItem(Item item) {
		if (item instanceof EntityItem
				&& ((EntityItem) item).getEntity() instanceof Annotation) {
			return (Annotation) ((EntityItem) item).getEntity();
		} else {
			return null;
		}
	}

	@Override
	public Object getIdForEntity(Annotation entity) {
		return entity != null ? entity.getId() : null;
	}

}
